/*
 * NOTICE: THE FILE HAS BEEN MODIFIED TO SUIT THE NEEDS OF THE PROJECT.
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.robobinding.internal.java_beans;



/**
 * The <code>Introspector</code> is a utility for developers to figure out which
 * properties, events, and methods a JavaBean supports.
 * <p>
 * The <code>Introspector</code> class walks over the class/superclass chain of
 * the target bean class. At each level it checks if there is a matching
 * <code>BeanInfo</code> class which provides explicit information about the
 * bean, and if so uses that explicit information. Otherwise it uses the low
 * level reflection APIs to study the target class and uses design patterns to
 * analyze its behaviour and then proceeds to continue the introspection with
 * its baseclass.
 * </p>
 * <p>
 * To look for the explicit information of a bean:
 * </p>
 * <ol>
 * <li>The <code>Introspector</code> appends "BeanInfo" to the qualified name of
 * the bean class, try to use the new class as the "BeanInfo" class. If the
 * "BeanInfo" class exsits and returns non-null value when queried for explicit
 * information, use the explicit information</li>
 * <li>If the first step fails, the <code>Introspector</code> will extract a
 * simple class name of the bean class by removing the package name from the
 * qualified name of the bean class, append "BeanInfo" to it. And look for the
 * simple class name in the packages defined in the "BeanInfo" search path (The
 * default "BeanInfo" search path is <code>sun.beans.infos</code>). If it finds
 * a "BeanInfo" class and the "BeanInfo" class returns non-null value when
 * queried for explicit information, use the explicit information</li>
 * </ol>
 * 
 */
// ScrollPane cannot be introspected correctly
public class Introspector extends java.lang.Object {

	// Public fields
	/**
	 * Constant values to indicate that the <code>Introspector</code> will
	 * ignore all <code>BeanInfo</code> class.
	 */
	public static final int IGNORE_ALL_BEANINFO = 3;

	/**
	 * Constant values to indicate that the <code>Introspector</code> will
	 * ignore the <code>BeanInfo</code> class of the current bean class.
	 */
	public static final int IGNORE_IMMEDIATE_BEANINFO = 2;

	/**
	 * Constant values to indicate that the <code>Introspector</code> will use
	 * all <code>BeanInfo</code> class which have been found. This is the
	 * default one.
	 */
	public static final int USE_ALL_BEANINFO = 1;

	// Default search path for BeanInfo classes
	private static final String DEFAULT_BEANINFO_SEARCHPATH = "sun.beans.infos"; //$NON-NLS-1$

	// The search path to use to find BeanInfo classes
	// - an array of package names that are used in turn
	private static String[] searchPath = { DEFAULT_BEANINFO_SEARCHPATH };

	// The cache to store Bean Info objects that have been found or created
	//private static final int DEFAULT_CAPACITY = 128;

	//private static Map<Class<?>, StandardBeanInfo> theCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, StandardBeanInfo>(DEFAULT_CAPACITY));
	private static WeakCache<Class<?>, StandardBeanInfo> theCache = new WeakCache<Class<?>, StandardBeanInfo>();

	private Introspector() {
		super();
	}

	/**
	 * Gets the <code>BeanInfo</code> object which contains the information of
	 * the properties, events and methods of the specified bean class.
	 * 
	 * <p>
	 * The <code>Introspector</code> will cache the <code>BeanInfo</code>
	 * object. Subsequent calls to this method will be answered with the cached
	 * data.
	 * </p>
	 * 
	 * @param beanClass
	 *            the specified bean class.
	 * @return the <code>BeanInfo</code> of the bean class.
	 * @throws IntrospectionException
	 */
	public static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
		StandardBeanInfo beanInfo = theCache.get(beanClass);
		if (beanInfo == null) {
			beanInfo = getBeanInfoImplAndInit(beanClass, null, USE_ALL_BEANINFO);
			theCache.put(beanClass, beanInfo);
		}
		return beanInfo;
	}

	private static StandardBeanInfo getBeanInfoImpl(Class<?> beanClass, Class<?> stopClass, int flags) throws IntrospectionException {
		BeanInfo explicitInfo = null;
		if (flags == USE_ALL_BEANINFO) {
			explicitInfo = getExplicitBeanInfo(beanClass);
		}
		StandardBeanInfo beanInfo = new StandardBeanInfo(beanClass, explicitInfo, stopClass);

		if (beanInfo.additionalBeanInfo != null) {
			for (int i = beanInfo.additionalBeanInfo.length - 1; i >= 0; i--) {
				BeanInfo info = beanInfo.additionalBeanInfo[i];
				beanInfo.mergeBeanInfo(info, true);
			}
		}

		// recursive get beaninfo for super classes
		Class<?> beanSuperClass = beanClass.getSuperclass();
		if (beanSuperClass != stopClass) {
			if (beanSuperClass == null)
				throw new IntrospectionException("Stop class is not super class of bean class"); //$NON-NLS-1$
			int superflags = flags == IGNORE_IMMEDIATE_BEANINFO ? USE_ALL_BEANINFO : flags;
			BeanInfo superBeanInfo = getBeanInfoImpl(beanSuperClass, stopClass, superflags);
			if (superBeanInfo != null) {
				beanInfo.mergeBeanInfo(superBeanInfo, false);
			}
		}
		return beanInfo;
	}

	private static BeanInfo getExplicitBeanInfo(Class<?> beanClass) {
		String beanInfoClassName = beanClass.getName() + "BeanInfo"; //$NON-NLS-1$
		try {
			return loadBeanInfo(beanInfoClassName, beanClass);
		} catch (Exception e) {
			// fall through
		}

		int index = beanInfoClassName.lastIndexOf('.');
		String beanInfoName = index >= 0 ? beanInfoClassName.substring(index + 1) : beanInfoClassName;
		BeanInfo theBeanInfo = null;
		BeanDescriptor beanDescriptor = null;
		for (int i = 0; i < searchPath.length; i++) {
			beanInfoClassName = searchPath[i] + "." + beanInfoName; //$NON-NLS-1$
			try {
				theBeanInfo = loadBeanInfo(beanInfoClassName, beanClass);
			} catch (Exception e) {
				// ignore, try next one
				continue;
			}
			beanDescriptor = theBeanInfo.getBeanDescriptor();
			if (beanDescriptor != null && beanClass == beanDescriptor.getBeanClass()) {
				return theBeanInfo;
			}
		}
		if (BeanInfo.class.isAssignableFrom(beanClass)) {
			try {
				return loadBeanInfo(beanClass.getName(), beanClass);
			} catch (Exception e) {
				// fall through
			}
		}
		return null;
	}

	/*
	 * Method which attempts to instantiate a BeanInfo object of the supplied
	 * classname
	 * 
	 * @param theBeanInfoClassName - the Class Name of the class of which the
	 * BeanInfo is an instance
	 * 
	 * @param classLoader
	 * 
	 * @return A BeanInfo object which is an instance of the Class named
	 * theBeanInfoClassName null if the Class does not exist or if there are
	 * problems instantiating the instance
	 */
	private static BeanInfo loadBeanInfo(String beanInfoClassName, Class<?> beanClass) throws Exception {
		try {
			ClassLoader cl = beanClass.getClassLoader();
			if (cl != null) {
				return (BeanInfo) Class.forName(beanInfoClassName, true, beanClass.getClassLoader()).newInstance();
			}
		} catch (Exception e) {
			// fall through
		}
		try {
			return (BeanInfo) Class.forName(beanInfoClassName, true, ClassLoader.getSystemClassLoader()).newInstance();
		} catch (Exception e) {
			// fall through
		}
		return (BeanInfo) Class.forName(beanInfoClassName, true, Thread.currentThread().getContextClassLoader()).newInstance();
	}

	private static StandardBeanInfo getBeanInfoImplAndInit(Class<?> beanClass, Class<?> stopClass, int flag) throws IntrospectionException {
		StandardBeanInfo standardBeanInfo = getBeanInfoImpl(beanClass, stopClass, flag);
		standardBeanInfo.init();
		return standardBeanInfo;
	}

	/**
	 * Decapitalizes a given string according to the rule:
	 * <ul>
	 * <li>If the first or only character is Upper Case, it is made Lower Case
	 * <li>UNLESS the second character is also Upper Case, when the String is
	 * returned unchanged.
	 * </ul>
	 * 
	 * @param name
	 *            - the String to decapitalize
	 * @return the decapitalized version of the String
	 */
	public static String decapitalize(String name) {

		if (name == null)
			return null;
		// The rule for decapitalize is that:
		// If the first letter of the string is Upper Case, make it lower case
		// UNLESS the second letter of the string is also Upper Case, in which
		// case no
		// changes are made.
		if (name.length() == 0 || (name.length() > 1 && Character.isUpperCase(name.charAt(1)))) {
			return name;
		}

		char[] chars = name.toCharArray();
		chars[0] = Character.toLowerCase(chars[0]);
		return new String(chars);
	}
}
